
/*
 * (a)  (C) 1990 by Adobe Systems Incorporated. All rights reserved.
 *
 * (b)  If this Sample Code is distributed as part of the Display PostScript
 *	System Software Development Kit from Adobe Systems Incorporated,
 *	then this copy is designated as Development Software and its use is
 *	subject to the terms of the License Agreement attached to such Kit.
 *
 * (c)  If this Sample Code is distributed independently, then the following
 *	terms apply:
 *
 * (d)  This file may be freely copied and redistributed as long as:
 *	1) Parts (a), (d), (e) and (f) continue to be included in the file,
 *	2) If the file has been modified in any way, a notice of such
 *      modification is conspicuously indicated.
 *
 * (e)  PostScript, Display PostScript, and Adobe are registered trademarks of
 *	Adobe Systems Incorporated.
 * 
 * (f) THE INFORMATION BELOW IS FURNISHED AS IS, IS SUBJECT TO
 *	CHANGE WITHOUT NOTICE, AND SHOULD NOT BE CONSTRUED
 *	AS A COMMITMENT BY ADOBE SYSTEMS INCORPORATED.
 *	ADOBE SYSTEMS INCORPORATED ASSUMES NO RESPONSIBILITY
 *	OR LIABILITY FOR ANY ERRORS OR INACCURACIES, MAKES NO
 *	WARRANTY OF ANY KIND (EXPRESS, IMPLIED OR STATUTORY)
 *	WITH RESPECT TO THIS INFORMATION, AND EXPRESSLY
 *	DISCLAIMS ANY AND ALL WARRANTIES OF MERCHANTABILITY, 
 *	FITNESS FOR PARTICULAR PURPOSES AND NONINFRINGEMENT
 *	OF THIRD PARTY RIGHTS.
 */

/*
 * 	Document.m
 *
 *	Portions of the source code in this file are based on source code
 *	from the Draw example application provided by NeXT.
 *
 *	The Document class serves to keep track of the global information
 *	concerning a particular file. It sets up the window and the views
 *	(ScrollView, DocView and DrawingView) . It manages the name
 *	of the file as well as save and print info information.
 *
 *	The listenerId is used to allow the user to drag an icon representing
 *	a PostScript file into the document.  The iconPathList is the
 *	list of files which was last dragged into the document.
 *
 *	The saved variable marks whether a disk file has been
 *	created for the document (i.e. if it has ever been saved).
 *
 *	Version:	2.0
 *	Author:	Ken Fromm
 */

#import "Document.h"
#import "ImportApp.h"
#import "DocView.h"
#import "DrawingView.h"
#import "ScrollingView.h"
#import "ImportPanel.h"
#import "SaveAsPanel.h"

#import <appkit/Cursor.h>
#import <appkit/Listener.h>
#import <appkit/Matrix.h>
#import <appkit/PageLayout.h>
#import <appkit/PrintInfo.h>
#import <appkit/ScrollView.h>
#import <appkit/Speaker.h>
#import <appkit/nextstd.h>
#import <appkit/publicWraps.h>
#import <objc/hashtable.h>
#import <string.h>

const NXRect			DefaultContentRect = {0.0, 0.0, 575.0, 660.0};
static const char 		DefaultName[] = "Empty Window";

extern void resizeBuffer();

@implementation Document

/* Factory methods */

/*
 * Creates a new, empty, document.
 *
 * Creates a PrintInfo object; creates a view whose size depends on the
 * default PrintInfo created; creates a window for that view; sets self
 * as the window's delegate; orders the window front; registers the window
 * with the Workspace Manager.  The default margins are set to 1/4 inch.
 */
+ new
{
	self = [super new];

	printinfoId = [PrintInfo  new];
	[printinfoId setMarginLeft:18.0 right:18.0 top:18.0 bottom:18.0];

	[self  createWindow];
	[self  setName:NULL andDirectory:NULL];
	[self  setDocument];

	return self;
}

/* Opens an existing document from the specified file. */
+ newFromFile:(const char *)file
{
	NXStream		*stream;

	self = [super new];
	stream = NXMapFile(file, NX_READONLY);

	[self  createWindow];				
	if ([self  readFromStream:stream])
	{
		[self  setName:file];
		[self  setDocument];
	}
	else
	{
		[self  free];
		self = nil;
	}
		
	return self;
}

/* Very private instance method needed by factory methods */

/*
 *	This method loads an archived document from the
 *	specified file name. The frame size is read first and then
 *	the window is displayed before the rest of the document
 *	is loaded (the print info and the drawing view).
 *	
 *	Does not place newView as the window's drawing view until
 *	the read is complete. If a failure occurs for some reason
 *	during the read, the previous drawing view is retained.
 *
 *	An NX_DURING handler is needed around the NXTypedStream
 *	operations because if the user has asked that a bogus file be
 *	opened, the NXTypedStream will raise an error.  To handle the
 *	error, the NXTypedStream must be closed.
 */
- readFromStream:(NXStream *)stream
{
	BOOL				err = YES;

	NXTypedStream		*volatile ts = NULL;

	if (stream)
	{
		NX_DURING
			ts = NXOpenTypedStream(stream, NX_READONLY);
			if (ts)
			{
				printinfoId = NXReadObject(ts);
				drawingviewId = NXReadObject(ts);
				NXCloseTypedStream(ts);
				err = NO;
			}
		NX_HANDLER
			NXCloseTypedStream(ts);
		NX_ENDHANDLER

		NXClose(stream);
	}

	if (!err)
	{
		[[docviewId  setDrawView:drawingviewId]  free];
		[docviewId  placeView:drawingviewId];
		[[windowId  contentView]  display];
	}
	else
		Notify("Open Error", "Cannot open file.");

	return self;
}

- setDocument
{
	NXPoint		location;

	NXRect		winFrame;

	[docviewId  placeView:drawingviewId];

	[self  registerWindow];
	[windowId  setDelegate:self];
	[windowId  display];
	
	[windowId  getFrame:&winFrame];
	[NXApp  getPosition:&location  forSize:&winFrame.size];
	[windowId  moveTo:location.x  :location.y];

	[windowId  makeKeyAndOrderFront:self];

	return self;
}

- free
{
	[printinfoId free];
	[windowId free];

	NX_FREE(name);
	NX_FREE(directory);
	NX_FREE(iconPathList);

	return [super free];
}

/*
*	Create the drawing window and place a scrollview as the content view.
*	A DrawingView instance is placed as the DocView of the ScrollView.
*/
- createWindow
{
	id				scrollView;

	const NXRect		*paperRect;

	windowId = [Window newContent:&DefaultContentRect
			style:NX_TITLEDSTYLE
			backing:NX_BUFFERED
			buttonMask:NX_ALLBUTTONS
			defer:NO];
	[windowId  addToEventMask:NX_FLAGSCHANGEDMASK];

	scrollView = [ScrollingView  newFrame:&DefaultContentRect];
	[scrollView  setBorderType:SCROLLVIEW_BORDER];

	paperRect =  [printinfoId  paperRect];
	drawingviewId = [DrawingView  newFrame:paperRect];
	docviewId = [[[[DocView new]  setClipping:NO] setScale:1.0]  setFlipped:NO];
	[docviewId  notifyAncestorWhenFrameChanged:YES];

	[docviewId  setDrawView:drawingviewId];
	[scrollView  setDocView:docviewId];
	[[docviewId  superview]  allocateGState];

	[[windowId  setContentView:scrollView] free];

	[windowId  makeFirstResponder:drawingviewId];

	return self;
}

- window
{
    return windowId;
}

/*  Returns the DrawingView associated with this document.  */
- drawingView
{
    return drawingviewId;
}

- docView
{
    return docviewId;
}

- printInfo
{
	return printinfoId;
}

/* Target/Action methods */
/*
 * Puts up a PageLayout panel and allows the user to pick a different
 * size paper to work on.  The view is then resized to the
 * new paper size. The new DrawingView is placed in the DocView
 * and then scrolled to the previous rectangle.
 * The view is dirtied because the PrintInfo is part of the document.
 */
- changeLayout:sender
{
	NXRect			visibleRect;
	const NXRect		*paperRect;

	if ([[PageLayout new] runModal] == NX_OKTAG)
	{
		[drawingviewId getVisibleRect:&visibleRect];
		paperRect = [printinfoId  paperRect];
		[drawingviewId sizeTo:paperRect->size.width :paperRect->size.height];
		[docviewId  placeView:drawingviewId];
		[drawingviewId scrollRectToVisible:&visibleRect];
		[[windowId  contentView] display];
		[drawingviewId setDirty:YES];
	}

	return self;
}

- print:sender
{
	return [drawingviewId  printPSCode:sender];
}

/*  Revert the document back to what is on the disk.  */ 
- revertToSaved:sender
{
	NXStream		*stream;
	NXRect			visibleRect;
	const NXRect		*paperRect;

	if (!saved || ![drawingviewId isDirty] ||
		(NXRunAlertPanel("Revert",
			"%s has been edited.  Are you sure you want to undo changes?", 
			"Revert", "Cancel", NULL, name) != NX_ALERTDEFAULT))
	{
		return self;
	}

	[drawingviewId getVisibleRect:&visibleRect];
	[windowId endEditingFor:self];

	stream = NXMapFile([self filename], NX_READONLY);
	if (stream && [self readFromStream:stream])
	{
		[windowId disableDisplay];
			paperRect = [printinfoId  paperRect];
			[drawingviewId sizeTo:paperRect->size.width :paperRect->size.height];
			[docviewId  placeView:drawingviewId];
			[drawingviewId scrollRectToVisible:&visibleRect];
			[windowId reenableDisplay];
		[[windowId contentView]  display];
		[windowId makeFirstResponder:drawingviewId];
		[drawingviewId setDirty:NO];
		NXClose(stream);
	}
	else
	{
		if (stream)
			NXClose(stream);
		Notify("Revert Error", "Cannot revert to saved file.");
	}

	return self;
}

/*
*	Bring up the import (open) panel to obtain the file and then pass
*	to the drawing view.
*/

- import:sender
{
	id						importpanel;

	static const char *const		filetype[4] = {"ps", "eps", "tiff", NULL};

	importpanel = [[ImportPanel  new]  setImport];
	if ([importpanel runModalForTypes:filetype])
		[drawingviewId  importFile:[importpanel  filename]  at:NULL];

	return self;
}

/*
 *	Writes out the document in archive format.
 *	(Saves the PrintInfo and DrawingView objects. See
 *	DrawingView's write: methods for more details.)
 */
- saveFile:(const char *) file
{
	BOOL			error;

	NXTypedStream	*typedstream;

	error = YES;
	typedstream = NXOpenTypedStreamForFile(file, NX_WRITEONLY);
	if (typedstream)
	{
		NXWriteRootObject(typedstream, printinfoId);
		NXWriteRootObject(typedstream, drawingviewId);
		NXCloseTypedStream(typedstream);
		error = NO;
	}
	else
		Notify("Save Error", "Cannot open a typed stream to the file.");

	return (error ? nil : self);
}

/*
 * Saves the file.  If this document has never been saved to disk,
 * then a SavePanel is put up to ask the user what file name she
 * wishes to use to save the document.
 */
- save:sender
{
	id		savepanel;

	if ([drawingviewId isDirty] || !saved)
	{
		if (!saved)
		{
			savepanel = [[SaveAsPanel  new]  setSave];
			if (![savepanel	runModalForDirectory:directory  file:NULL])
				return self;

			[self  setName:[savepanel  filename]];
		}

		if ([self  saveFile:[self  filename]])
		{
			[drawingviewId  setDirty:NO];
			saved = YES;
		}
	}

	return self;
}

- saveAs:sender
{
	id		savepanel;

	char		*tempname;

	if (strcmp(name, DefaultName) == 0)
		tempname = NULL;
	else
		tempname = name;

	savepanel = [[SaveAsPanel  new]  setSaveAs];
	if ([savepanel	runModalForDirectory:directory  file:tempname])
	{
		[drawingviewId setDirty:YES];
		[self  setName:[savepanel  filename]];
		if ([self  saveFile:[self  filename]])
		{
			[drawingviewId  setDirty:NO];
			saved = YES;
		}
	}
	
	return self;
}

 /*
 *	Writes out the document in Epsf/Illustrator format.
 */
- saveTo:sender
{
	id			savepanel;

	NXStream	*stream;

	savepanel = [[SaveAsPanel  new]  setSaveTo];
	if ([savepanel	runModal])
	{	
		stream = NXOpenMemory(NULL, 0, NX_WRITEONLY);
		if (stream)
		{
			[drawingviewId  writePSToStream:stream];
			NXSaveToFile(stream, [savepanel  filename]);
			NXCloseMemory(stream, NX_FREEBUFFER);
		}
		else
			Notify("Save Error", "Cannot open a stream to the file.");
	}

	return self;
}

/* Methods related to naming/saving this document. */
/*
 *	Gets the fully specified file name of the document.
 */
- (const char *)filename
{
	static char		filenamebuf[MAXPATHLEN+1];

	if (!directory || !name)
		[self  setName:name andDirectory:directory];

	if (directory)
	{
		strcpy(filenamebuf, directory);
		strcat(filenamebuf, "/");
	}
	else
		filenamebuf[0] = '\0';

	if (name)
		strcat(filenamebuf, name);

	return filenamebuf;
}

- (const char *)directory
{
	return directory;
}

- (const char *)name
{
	return name;
}

/*
 * If file is a full path name, then both the name and directory of the
 * document is updated appropriately, otherwise, only the name is changed.
 */
- setName:(const char *)file
{
	char		*lastComponent;
	char		path[MAXPATHLEN+1];

	if (file)
	{
		strcpy(path, file);
		lastComponent = strrchr(path, '/');
		if (lastComponent)
		{
			*lastComponent++ = '\0';
			return [self setName:lastComponent andDirectory:path];
		}
		else
			return [self setName:file andDirectory:NULL];
	}

	return self;
}

/*
 * Updates the name and directory of the document.
 * newName or newDirectory can be NULL, in which case the name or directory
 * will not be changed (unless one is currently not set, in which case
 * a default name will be used).
 */
- setName:(const char *)newName andDirectory:(const char *)newDirectory
{
	if ((newName && *newName) || !name)
	{
		if (!newName || !*newName)
			newName = DefaultName;
		NX_FREE(name);
		name = NXCopyStringBuffer(newName);
	}

	if ((newDirectory && (*newDirectory == '/')) || !directory)
	{
		if (!newDirectory || (*newDirectory != '/'))
			newDirectory = [NXApp currentDirectory];
		NX_FREE(directory);
		directory = NXCopyStringBufferFromZone(newDirectory, [self zone]);
	}
	[windowId  setTitleAsFilename:[self  filename]];

	return self;
}

/* Window delegate methods. */

/*
 * Switch the Application's PrintInfo to the document's when the document
 * window becomes the main window.  Also set the cursor appropriately
 * depending on which tool is currently selected.
 */
- windowDidBecomeMain:sender
{
	[NXApp setPrintInfo:printinfoId];
	[self  resetResponder];

	return self;
}

/*
 *	Prevents the window from getting too large or too small.
 */
- windowWillResize:sender toSize:(NXSize *)size
{
	NXSize	screenSize;
	
	[NXApp  getScreenSize:&screenSize];
	screenSize.width = screenSize.width * 0.90;
	screenSize.height = screenSize.height * 0.90;

	size->width = MIN(screenSize.width, size->width);
	size->height = MIN(screenSize.height, size->height);
	
	size->width = MAX(MIN_WINDOW_WIDTH, size->width);
	size->height = MAX(MIN_WINDOW_HEIGHT, size->height);

	return self;
}

/*
 * Resizes the doc view and repositions the drawing view inside the doc view.
 */
- windowDidResize:sender
{
	NXRect		frameRect, contRect;

	[windowId  getFrame:&frameRect];
	[Window  getContentRect:&contRect  forFrameRect:&frameRect  style:[windowId  style]];
	resizeBuffer([drawingviewId  buffer], &contRect.size);

	[docviewId  placeView:drawingviewId];

	return self;
}

/*
 * If the GraphicView has been edited, then this asks the user if she
 * wants to save the changes before closing the window.  When the window
 * is closed, the DrawDocument itself must be freed.  This is accomplished
 * via Application's delayedFree: mechanism.  Unfortunately, by the time
 * delayedFree: frees the DrawDocument, the window and view instance variables
 * will already have automatically been freed by virtue of the window's being
 * closed.  Thus, those instance variables must be set to nil to avoid their
 * being freed twice.
 *
 * Returning nil from this method informs the caller that the window should
 * NOT be closed.  Anything else implies it should be closed.
 */
- windowWillClose:sender
{
	int save;

	if ([drawingviewId isDirty] && (saved || ![drawingviewId isEmpty]))
	{
		save = NXRunAlertPanel("Close", "%s has changes. Save them?", "Save",
				"Don't Save", "Cancel", name);
		if (save == NX_ALERTDEFAULT || save == NX_ALERTALTERNATE)
		{
			[sender endEditingFor:self];	/* terminate any editing */
			if (save == NX_ALERTDEFAULT)
				[self save:nil];
		}
		else
			return nil;
	}

	[self unregisterWindow];

	[NXApp setPrintInfo:nil];

	return self;
}

/* Icon dragging methods */

/*
 * Registers the document window with the Workspace Manager so that when the
 * user picks up an icon in the Workspace and drags it over our document window
 * and lets go, iconEntered:... and iconReleasedAt::ok: messages will be
 * sent to the DrawDocument from the Workspace Manager.  Allows the user to
 * drag PostScript and TIFF files into the document.
 */
- registerWindow
{
	unsigned int	windowNum;
	id			speaker = [NXApp appSpeaker];

	listenerId = [Listener new];
	[listenerId setDelegate:self];
	[listenerId usePrivatePort];
	[listenerId addPort];
    
	NXConvertWinNumToGlobal([windowId windowNum], &windowNum);
	[speaker setSendPort:NXPortFromName(NX_WORKSPACEREQUEST, NULL)];
	[speaker registerWindow:windowNum toPort:[listenerId listenPort]];

	return self;
}

/*  Undoes what registerWindow does.  */
- unregisterWindow
{
	unsigned int		windowNum;
	id				speaker = [NXApp appSpeaker];

	if (listenerId)
	{
		[speaker  setSendPort:NXPortFromName(NX_WORKSPACEREQUEST, NULL)];
		NXConvertWinNumToGlobal([windowId windowNum], &windowNum);
		[speaker  unregisterWindow:windowNum];
		[listenerId free];
	}

	return self;
}

/*
 * Called whenever an icon is dragged from the Workspace over the document
 * window.  At this point, all that is done is to salt away the list of files
 * represented by the icon.  All the real work is done in iconReleasedAt::ok:.
 */
- (int)iconEntered:(int)windowNum at:(double)x :(double)y
    iconWindow:(int)iconWindowNum iconX:(double)iconX iconY:(double)iconY
    iconWidth:(double)iconWidth iconHeight:(double)iconHeight
    pathList:(char *)pathList
{
	if (!iconPathList || strcmp(iconPathList, pathList))
	{
		NX_FREE(iconPathList);
		NX_MALLOC(iconPathList, char, strlen(pathList)+1);
		strcpy(iconPathList, pathList);
	}
	return 0;
}

/*
 * Goes through the list of files associated with the icon dragged
 * from the Workspace and checks if any of them are PostScript or TIFF.
 * If any are, then the GraphicView is asked to load those in as objects.
 * Very important: an NX_DURING handler is required around all the processing
 * of this method since an uncaught raised error will cause this method not
 * to return and thus hang the Workspace Manager for a while.
 */
- (int)iconReleasedAt:(double)x :(double)y ok:(int *)flag
{
	volatile int		foundOne = NO;
	char				*file, *tab, *extension;

	int				format;

	id				importpanel;

	NXPoint			pt;

	NX_DURING
		importpanel = [ImportPanel  new];
		format = [importpanel  format];
		[importpanel  setFormat:IMPORT_COPY];

		pt.x = x;
		pt.y = y;
		[windowId  convertScreenToBase:&pt];
		[drawingviewId  convertPoint:&pt fromView:nil];
		file = iconPathList;
		while (file)
		{
			tab = strchr(file, '\t');
			if (tab)
				*tab = '\0';
			extension = strrchr(file, '.');
			if (extension)
			{
				if (!strcmp(extension, ".ps") ||
					!strcmp(extension, ".eps") ||
						!strcmp(extension, ".tiff"))
					if ([drawingviewId  importFile:file  at:&pt])
						foundOne = YES;
			}
			file = tab ? tab++ : NULL;
		}
		if (foundOne)
		{
			[NXApp activateSelf:YES];
			[windowId makeKeyAndOrderFront:self];
		}
		[importpanel  setFormat:format];
	NX_HANDLER
	NX_ENDHANDLER

	*flag = foundOne;

	return 0;
}

/* Validates whether a menu command makes sense now */

/*
 * Validates whether a menu command that DrawDocument responds to
 * is valid at the current time.
 */
- (BOOL)validateCommand:menuCell
{
	SEL action = [menuCell action];

	if (action == @selector(save:))
		return ([drawingviewId  isDirty] || !saved);
	else if (action == @selector(saveTo:))
		return ([drawingviewId  isSelected]);
	else if (action == @selector(revertToSaved:))
		return ([drawingviewId  isDirty] && saved);

	return YES;
}

/* Cursor-setting method */

/*
 *	Resets the document's cursor rectangle to be the frame of the
 *	drawing view.
 *	Makes the drawing view the first responder if
 *	there isn't one or if no tool is selected.
 */
- resetResponder
{
	id			responder;

	responder = [windowId  firstResponder];
	if (!responder || responder == windowId || [NXApp  cursor] == NXArrow)
		[windowId makeFirstResponder:drawingviewId];

	return self;
}

@end

